import fg from 'fast-glob';
import fs from 'fs-extra';
import path from "path";
import { BaseTask, TaskResult } from "../../common/tasks/BastTask.js";
import { PrebuildAppData, PreBuildAppTask } from '../../common/tasks/PreBuildAppTask.js';
import { toolchain } from '../../toolchain.js';
import { Cmd } from '../../tools/Cmd.js';
import { signatureGet } from 'portable-executable-signature';
import { writeUTF8withBom, makeArchive } from '../../tools/vendor.js';
import { ExeInfo, Nullable } from '../../typings';
import { sendBuildFailureAlert } from '../../tools/alert.js';

export class PCBuildUnityTask extends BaseTask {
    async run(): Promise<TaskResult<void>> {    
        const prebuildResult = toolchain.taskHelper.getResult<PrebuildAppData>(PreBuildAppTask.name);
        
        const channelCfg = toolchain.params.channelCfg;

        const versionCode = prebuildResult!.data!.versionCode;

        const fullVersion = toolchain.params.version + '.' + versionCode;
        const exeName = channelCfg.exe!;
        const exportRoot = path.join(toolchain.params.workSpacePath, '../binPC', exeName);
        const exeSource = path.join(exportRoot, exeName + '.exe');
        const streamingConfigPath = path.join(exportRoot,exeName+'_Data','StreamingAssets','cconfig.txt');
        const packUrl = path.join(toolchain.params.workSpacePath, '../binPC');
        if(fs.existsSync(packUrl))
          await fs.emptyDir(packUrl);
        else
          await fs.mkdir(packUrl);
        
        console.log('@@ build channel PC exe ' + exeSource);
        await this._buildPC(prebuildResult!.data!.defines, fullVersion, exeSource);
        if(!fs.existsSync(exeSource)) {
            await sendBuildFailureAlert(`error: not find pc exe, build ${exeSource} failed!`);
            process.exit(1);
        }

        // 删除IL2CPP源码目录
        const il2cppSrc = path.join(exportRoot, `${exeName}_BackUpThisFolder_ButDontShipItWithYourGame`);
        await fs.remove(il2cppSrc);
            
        // copy WebBrowserTools.exe
        const wbtname = 'WebBrowserTools.exe';
        const toolPath = path.join(this.cmdOption.oldToolsRoot, 'FYMGBuild/PC');
        fs.copyFileSync(path.join(toolPath, wbtname), path.join(exportRoot, wbtname));
        
        // 修改exe信息
        await this.modifyExeVersionInfo(exeSource, fullVersion);
        // 附加签名
        // GameAssembly.dll附加时间戳失败
        await this.ensureSignature(['**/GameAssembly.dll'], exportRoot, false);
        await this.ensureSignature(['**/*.exe', '**/*.dll', '!**/GameAssembly.dll'], exportRoot, true);
        
        // 将配置中附加的文件copy到 binPC 中一同打包在zip中
        const pkgAdditional = channelCfg.pkgAdditional;
        if(pkgAdditional)
            fs.copySync(path.join(toolchain.params.workSpacePath, 'platform', pkgAdditional), path.join(packUrl, exeName));

        // 将apk保存到hudson，供内网下载
        const uploadDir = path.join(toolchain.params.uploadPath, 'exe', channelCfg.path);
        await fs.ensureDir(uploadDir);
        await fs.emptyDir(uploadDir);

        const pcChannelList = channelCfg.pcChannelList;
        for(let channel of pcChannelList) {
            const channelid = channel.id;
            writeUTF8withBom(streamingConfigPath, channelid);
            const saveExeName = exeName + '.pc.' + channelid + versionCode;
            const exeTarget = path.join(toolchain.params.packageLocalWebPath, saveExeName + '.zip');
            
            // 删除pdb文件
            const pdbs = await fg('*.pdb', { cwd: exportRoot });
            for (const p of pdbs) {
                await fs.remove(path.join(exportRoot, p));
            }
            
            console.log(`@@ Making archive from ${packUrl} and save as ${exeTarget}`);
            await makeArchive(exeTarget, 'zip', path.join(packUrl, exeName));
            this.updateApkVerInfoCfg(fullVersion, 0, toolchain.params.packageLocalWebPath, saveExeName + '.zip', 'exe', `exeinfo_${channelid}.json`);

            // 复制到exe文件夹下面，这样资源外发会顺便发到服务器上
            const zipPath = path.join(toolchain.params.packageLocalWebPath, saveExeName + '.zip');
            fs.copyFileSync(zipPath, path.join(uploadDir, saveExeName + '.zip'));
        }

        return { success: true, errorCode: 0 };
    }
        
    private async _buildPC(defines: string, version: string, exePath: string) {
        const params = ['-batchmode', '-projectPath', toolchain.params.workSpacePath, 
        '-executeMethod', 'BuildCommand.BuildForPC', '-defines', defines, '-numberversion', version, 
        '-path', exePath, '-bundleId', toolchain.params.channelCfg.bundleId, '-log', 'false', 
        '-productName', toolchain.params.channelCfg.productName, '-quit'];
        await toolchain.unity.runUnityCommand(this.cmdOption, params, 'EXE');
    }

    /**
     * 修改exe的版本信息
     * @param exeSource 
     * @param newVer 
     */
    private async modifyExeVersionInfo(exeSource: string, newVer: string) {
        const channelCfg = toolchain.params.channelCfg;
        console.log(`modifyExeVersionInfo, channelCfg = ${JSON.stringify(channelCfg)}`);
        console.log(`modifyExeVersionInfo, exeSource = ${exeSource}, newVer = ${newVer}, cwd = ${process.cwd()}`);
        const toolPath = path.join(this.cmdOption.oldToolsRoot, 'FYMGBuild/PC');
        const rcFile = path.join(toolPath, 'version.rc');
        await toolchain.svnClient.revert(rcFile);
        
        const rhScriptFile = 'hackscript.txt';
        const tmpExeSource = exeSource.replace('.exe', '.tmp.exe');
        const scriptContent = `[FILENAMES]
Exe=${exeSource}
SaveAs=${tmpExeSource}	
[COMMANDS]
-delete VERSIONINFO
-add ${rcFile.replace('.rc', '.res')} VERSIONINFO
`;
        console.log(scriptContent);
        fs.writeFileSync(rhScriptFile, scriptContent, 'utf-8');
        
        // load rc
        // the rc file is in 'ucs-12 le bom'
        const rcContent = fs.readFileSync(rcFile, 'ucs-2');
        const matchObj = rcContent.match(/(\d+\.\d+\.\d+\.\d+)/);
        if(matchObj) {
            const oldVer = matchObj[0]
            console.log(`old version is ${oldVer}`);
            if(!newVer) {
                const oldVerArr = oldVer.split('.');
                oldVerArr[3] = String(Number(oldVerArr[3]) + 1);
                newVer = oldVerArr.join('.');
            }
        }
        let newRcContent = rcContent.replace(/(\d+\.\d+\.\d+\.\d+)/g, newVer)
        .replace(/(\d+\,\d+\,\d+\,\d+)/g, newVer.replace('.', ','))
        .replace(/\{exename\}/g, channelCfg.exe!)
        .replace(/\{productName\}/g, channelCfg.productName);
        console.log(`@@ modifyExeVersionInfo:\n${newRcContent}\n`);
        fs.writeFileSync(rcFile, newRcContent, 'ucs-2');
        // compile rc to res
        const rcTool = path.join(toolPath, 'rc');
        await new Cmd().run(rcTool, [rcFile]);
        // use resource hacker to modify the exe version info
        const rhTool = path.join(toolPath, 'ResHacker');
        await new Cmd().run(rhTool, ['-script', rhScriptFile]);
        
        await fs.remove(exeSource);
        await fs.rename(tmpExeSource, exeSource);
    }

    private async ensureSignature(pattern: string[], cwd: string, tr: boolean): Promise<void> {
        const toSignedFiles = await fg(pattern, { cwd });
        const files: string[] = [];
        for (const f of toSignedFiles) {
            const file = path.join(cwd, f);
            files.push(file);
        }

        const notSigneds: string[] = [];
        for (const f of files) {
            const data = await fs.readFile(f);
            const signature = signatureGet(data);
            if (signature != null) {
                console.log('already signed:', f);
            } else {
                notSigneds.push(f);
            }
        }
        const signTool = path.join(this.cmdOption.oldToolsRoot, 'FYMGBuild/PC/wosigncodecmd');
        // await new Cmd().run(signTool, ['sign', '/default', '/p', 'moby180kg', '/hide', '/c', '/tr', 'http://timestamp.digicert.com', '/file', tmpExeSource])
        const cmd = new Cmd();
        const psw = 'moby180kg'; //moby200kg
        const params = ['sign', '/tp', '16DD1DB2BAEFC6D2537B00F897AEC22D068E46CE', '/p', psw, '/hide', '/c'];
        if (tr) params.push('/tr', 'http://timestamp.digicert.com');
        params.push('/file', ...notSigneds);
        const ret = await cmd.run(signTool, params, { silent: true });
        console.log(cmd.output);

        let failed = false;
        if (ret == 0) {
            const mch = cmd.output.match(/Number of errors: (\d+)/);
            if (mch != null && mch[1] != '0') {
                failed = true;
            }
        } else {
            failed = true;
        }
        
        if (failed) {
            await sendBuildFailureAlert('签名失败');
            process.exit(1);
        }
    }

    /**
     * apkinfo.json 被用于检测是否有新包可以下载
     * @param fullVersion 
     * @param size 
     * @param uploadDir 
     * @param apkName 
     * @param platName 
     * @param jsonName 
     */
    protected updateApkVerInfoCfg(fullVersion: string, size: number, uploadDir: string, apkName: string, platName: string, jsonName = 'apkinfo.json') {
        const channelCfg = toolchain.params.channelCfg;
        const url = path.join(channelCfg.url, platName, channelCfg.path, apkName);
        const infoFile = path.join(uploadDir, jsonName);
        let info: ExeInfo;
        if(fs.existsSync(infoFile)) {
            info = fs.readJSONSync(infoFile, {encoding: 'utf-8'});
        } else {
            info = {"curver":{"fixver":"0.0.0.0", "ver":"0.0.0.0", "url":"", "size":0}};
        }
        let fixver = '0.0.0.0';
        if(info['curver']) {
            fixver = info['curver']['fixver'];
        }
        info['curver'] = {"fixver": fixver, "ver":fullVersion, "url":url, "size":size};
        fs.writeFileSync(infoFile, JSON.stringify(info), 'utf-8');
    }

    get dependencies(): Nullable<string[]> {
        return [PreBuildAppTask.name];
    }

    get skip(): boolean {
        return !this.cmdOption.buildExe;
    }
}